Lab 6: Network Driver
这一部分感觉最重要的就是对硬件的驱动过程有了一个更加深刻的认识,本实验相当于是去实现一个网卡的驱动,并且在这个驱动上,开发相应的应用程序(echosrv
和httpd
)。
首先是pci_init()
,这一部分主要就是扫描PIC总线上的一些设备,与系统内核中带有的驱动程序进行匹配。
通过PCI的扫描,我们发现qemu提供下面几种设别:
本实验主要是实现网卡E1000的驱动,通过查阅相关intel网卡的手册,发现Vendor ID和Device ID如下所示:
PCI主要就是通过上面的两个变量进行匹配的,一旦匹配,说明系统中存在该设备的驱动,就能够利用该设备了。
其他的设备由于没有在系统中完成相关的驱动程序,所以也没有设置Vendor ID和Device ID。
有了设备后,为了能够方便对设备进行操作,我们使用lab1提到的MMIO(VGA)和lab4中对多核CPU lapic的初始化。MMIO主要的作用就是像是操作内存一样的去操作硬件,实际上访问内存地址的时候,访问的是硬件的寄存器地址。
但是MMIO地址空间和大小并不是系统进行决定的,而是和设备进行协商进行确定(感觉是要是知道MMIO空间的大小),因此主要工作就是初始化这个结构体:
struct pci_func {
struct pci_bus *bus; // Primary bus for bridges
uint32_t dev;
uint32_t func;
uint32_t dev_id;
uint32_t dev_class;
uint32_t reg_base[6];//最多6个MMIO区域
uint32_t reg_size[6];//每个MMIO区域的大小
uint8_t irq_line;//中断的地址线,上面PCI线扫描显示网卡的中断使用的是11
};
然后系统通过这个结构体的信息,来初始化e1000信息,这些初始化的信息是直接写到硬件中的(虽然感觉像是操作内存变量)。
pci_e1000_attach(struct pci_func * pcif)
{
pci_func_enable(pcif);
init_desc();
e1000 = mmio_map_region(pcif->reg_base[0], pcif->reg_size[0]);
cprintf("e1000: bar0 %x size0 %x\n", pcif->reg_base[0], pcif->reg_size[0]);
e1000[TDBAL/4] = PADDR(tx_d);
e1000[TDBAH/4] = 0;
e1000[TDLEN/4] = TXRING_LEN * sizeof(struct tx_desc);
e1000[TDH/4] = 0;
e1000[TDT/4] = 0;
e1000[TCTL/4] = TCTL_EN | TCTL_PSP | (TCTL_CT & (0x10 << 4)) | (TCTL_COLD & (0x40 << 12));
e1000[TIPG/4] = 10 | (8 << 10) | (12 << 20);
//接收端的设置
e1000[RA/4] = mac[0];
e1000[RA/4+1] = mac[1];
e1000[RA/4+1] |= RAV;
cprintf("e1000: mac address %x:%x\n", mac[1], mac[0]);
memset((void*)&e1000[MTA/4], 0, 128 * 4);
e1000[ICS/4] = 0;
e1000[IMS/4] = 0;
//e1000[IMC/4] = 0xFFFF;
e1000[RDBAL/4] = PADDR(rx_d);
e1000[RDBAH/4] = 0;
e1000[RDLEN/4] = RXRING_LEN * sizeof(struct rx_desc);
e1000[RDH/4] = 0;
e1000[RDT/4] = RXRING_LEN - 1;
e1000[RCTL/4] = RCTL_EN | RCTL_LBM_NO | RCTL_SECRC | RCTL_BSIZE | RCTL_BAM;
cprintf("e1000: status %x\n", e1000[STATUS/4]);
return 1;
}
还需要说明的就是E1000网卡使用的是DMA技术与JOS进行交互。
也就是说,往内存里读写网络数据包不需要通过CPU,实际上还是通过MMIO这块区域进行操作。本实验基本上没有涉及DMA的代码,因此了解就好了。
因此,我们就能很轻松的使用这张网卡了。
下面是一些系统调用的实现。
首先是实现系统的定时中断。JOS中设置的是每250ms来让server进程检测是否有包要接受或者发送。(Linux系统中是4ms,精度可以调的更高,但是带来的开销也是越大的。从这里就可以看到Linux并不是一个实时的操作系统,而是一个分时操作系统,涉及到时间操作的精度并不是很高)。
然后实现发包,为什么呢?因为要是没有实现发包,那么收包也没有东西可以收。
这一部分的测试在/net/testoutput.c
中,可以结合着测试的样例和具体的实现原理来阅读代码。
然后实现的是input的模块。
有了上面的四个模块基础之后,之后将会在这四个模块的基础之上,实现两个应用:echosrv
和httpd
。
echosrv
主要的作用就是收到一个连接的信息后,返回相同的信息。
服务端:
make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-echosrv
客户端:
make nc-7
下面说一说echosrv
的建立连接的原理:
首先是向网络服务进程发送一个IPC请求,然后一直处于接受状态:
int res = ipc_recv(NULL, NULL, NULL);
该等待是基于事件进行触发的,因此如果没有请求来,那么根本不会占用任何的计算资源(因为ipc_recv是将进程的状态设置为了curenv->env_status = ENV_NOT_RUNNABLE,因此每一次轮询都不会执行该进程)。
同理,httpd
也是类似的作用,使用的是80端口进行服务,并且能通过url进行文件的访问。
这个实现的框架标记了实现的过程,结构如下: